Skip to content

Release v0.41.0#11279

Draft
lidel wants to merge 60 commits intoreleasefrom
release-v0.41.0
Draft

Release v0.41.0#11279
lidel wants to merge 60 commits intoreleasefrom
release-v0.41.0

Conversation

@lidel
Copy link
Copy Markdown
Member

@lidel lidel commented Apr 12, 2026

lidel and others added 30 commits February 11, 2026 04:25
- boxo includes code updates to modernize code by applying go fix modernizers from Go 1.26
- includes dependency updates with needed fixes
picks up recent fixes and maintenance:
- protect keystore size during reset (#1227)
- update dependencies and minimum go version (#1230)
- apply go fix modernizers from Go 1.26 (#1231)
- go-libdht org transfer (#1229)
- bump pion/dtls/v3 to v3.1.0 (#1232)
* Upgrade to Boxo v0.37.0
`name put` rejected republishing the exact same record because the
sequence check used `>=` which blocked the common use case of fetching
a third-party record and putting it back to refresh DHT availability.

allow putting identical records (same bytes) while still rejecting
different records with the same or lower sequence number. also add
a success message on put (suppressible with `--quiet`), and clarify
the error message to say "IPNS record" and reference `ipfs name put --force`.

Closes #11197
* docs: add AGENTS.md with instructions for AI coding agents

provide project context, Go style conventions, build/test commands,
and safety boundaries so AI-assisted contributions produce idiomatic,
well-tested code that follows existing patterns.

* chore: update AGENTS.md

Co-authored-by: Andrew Gillis <11790789+gammazero@users.noreply.github.com>

---------

Co-authored-by: Andrew Gillis <11790789+gammazero@users.noreply.github.com>
adds new DNSLink -> IPNS resolution tests for both gateway and
subdomain forms, covering base58btc peer ID and CIDv1 libp2p-key
representations.

ref: https://github.com/ipfs/gateway-conformance/releases/tag/v0.11.0
…11208)

otelhttp derives server.address from the Host header, which creates
a unique time series for every subdomain hostname on public gateways
(e.g. each CID.ipfs.dweb.link). this caused multi-gigabyte prometheus
responses and scrape timeouts.

- cmd/ipfs/kubo/daemon.go: add OTel SDK View that drops server.address
  from all http.server.* metrics at the MeterProvider level
- core/corehttp/gateway.go: add server.domain attribute to Gateway and
  HostnameGateway handlers, grouping by Gateway.PublicGateways suffix
  (e.g. "dweb.link"), with "localhost", "loopback", or "other" fallbacks
- core/corehttp/commands.go: add server.domain="api" to RPC handler
- core/corehttp/gateway.go: add server.domain="libp2p" to libp2p handler
- docs/changelogs/v0.40.md: add changelog highlight
- docs/metrics.md: document server_domain label and server_address drop
* fix: disable otel exemplars to prevent prometheus rune overflow

the OTel SDK View from #11208 drops server.address from http.server.*
metric labels, but the OTel spec requires filtered attributes to be
carried as exemplar FilteredAttributes. on subdomain gateways the
server.address value (e.g. "CID.ipfs.dweb.link") combined with
trace_id and span_id exceeds the 128-rune prometheus exemplar limit.

- cmd/ipfs/kubo/daemon.go: add exemplar.AlwaysOffFilter to MeterProvider
- docs/changelogs/v0.40.md: document exemplar disable in metrics section
- resolve version.go conflict: keep 0.41.0-dev from master
* docs: streamline release checklist ordering and dependencies

- add parallelism note clarifying artifact dependency graph
- deduplicate Docker wait (STOP gate covers it)
- note sync-release-assets dependency on dist.ipfs.tech
- move Companion smoke test into ipfs-desktop step (test against PR build)

* docs: improve release checklist clarity and conciseness

- front-load verbs in checklist items (action before location)
- tighten warning text with clear rationale (GPG signatures, audit trail)
- fix passive voice, cut filler phrases
- clarify version.go conflict resolution during merge-back
* Revert "feat: update to Go 1.26 (#11189)"

This reverts commit 36c29c5.

* chore: go mod tidy

* fix: keep go 1.25-compatible modernizations, add v0.40.1 changelog

- config/autoconf.go: restore math/rand/v2 (available since go 1.22)
- core/corehttp/p2p_proxy.go: restore httputil.ReverseProxy.Rewrite (available since go 1.21)
- core/commands/name/name.go: restore %d format for ValidityType int64
- docs/changelogs/v0.40.md: keep shipped v0.40.0 notes intact, add v0.40.1 section

---------

Co-authored-by: Marcin Rataj <lidel@lidel.org>
- core/corehttp/webui.go: bump WebUIPath CID to v4.12.0, add v4.11.1 to historical list
- docs/changelogs/v0.41.md: add webui improvements highlight (ipv6 geolocation, peers screen optimizations)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](docker/login-action@v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps): bump actions/upload-artifact from 6 to 7

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump actions/download-artifact from 7 to 8

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Marcin Rataj <lidel@lidel.org>
- point to canonical URLs instead of github PR links
- only updated active docs and v0.40 changelog
- left old changelogs and unmerged IPIPs as-is
…in the ipfs-ecosystem group (#11219)

* chore(deps): bump github.com/ipfs/go-ipld-legacy

Bumps the ipfs-ecosystem group with 1 update: [github.com/ipfs/go-ipld-legacy](https://github.com/ipfs/go-ipld-legacy).


Updates `github.com/ipfs/go-ipld-legacy` from 0.2.2 to 0.3.0
- [Release notes](https://github.com/ipfs/go-ipld-legacy/releases)
- [Commits](ipfs/go-ipld-legacy@v0.2.2...v0.3.0)

---
updated-dependencies:
- dependency-name: github.com/ipfs/go-ipld-legacy
  dependency-version: 0.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ipfs-ecosystem
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: run make mod_tidy

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andrew Gillis <11790789+gammazero@users.noreply.github.com>
Bumps [ipfs/gateway-conformance](https://github.com/ipfs/gateway-conformance) from 0.11 to 0.12.
- [Release notes](https://github.com/ipfs/gateway-conformance/releases)
- [Changelog](https://github.com/ipfs/gateway-conformance/blob/main/CHANGELOG.md)
- [Commits](ipfs/gateway-conformance@v0.11...v0.12)

---
updated-dependencies:
- dependency-name: ipfs/gateway-conformance
  dependency-version: '0.12'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](docker/build-push-action@v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](docker/setup-buildx-action@v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](docker/setup-qemu-action@v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* test: increase GetClosestPeers DHT timeout from 2m to 5m

passing runs finish in 8-48s, but WAN DHT bootstrap occasionally
fails entirely in CI (~4.3% flake rate on master), hitting the
2m ceiling. bumping to 5m gives more retry attempts without
affecting passing runs.

* test: fix flaky TestThreeLeggedCatTransfer

- add 3-minute context timeout instead of unbounded context.Background(),
  so failures produce a meaningful error instead of burning 10 minutes
- reduce data from 100 MB to 1 MB; the test verifies three-legged DHT
  discovery + bitswap transfer, not bulk throughput
- explicitly provide root CID to DHT before catter fetches, eliminating
  the race between async reprovider and immediate Get
* fix: validate --max-hamt-fanout CLI flag per UnixFS spec

the CLI flag bypassed the config validation in ValidateImportConfig,
allowing spec-noncompliant values (e.g. 3 or 999999) to be silently
accepted. validation now happens in the options layer, covering both
CLI and programmatic API usage.

* fix: simplify HAMT fanout constraint phrasing

Co-authored-by: Guillaume Michel <15075495+guillaumemichel@users.noreply.github.com>
…#11238)

* fix(core/commands/pin): return error if listing an invalid, but known, pin type

* test: add cli test for pin ls with known but non-listable type

Covers the case where --type=internal passes boxo's StringToMode
validation but is rejected by options.Pin.Ls.Type, which previously
caused a panic instead of returning an error.

---------

Co-authored-by: Marcin Rataj <lidel@lidel.org>
guillaumemichel and others added 29 commits March 19, 2026 11:27
* fix(provider): purge keystore datastore after reset

* changelog

* use MapDatastore if no datastore is configured

* bump kad-dht to latest commit

* purge orphaned keystore migration

* bump kad-dht

* use main datastore for keystore "meta" store

* add provider/keystore/0 and /1 to ipfs diag command

mount keystore datastores to /provider/keystore/0 and /1 so that they
are included in the ipfs diag datastore command

* fix(provider): reject unexpected keystore suffix to prevent stray deletions

destroyDs calls os.RemoveAll with a suffix from the upstream library.
If suffix were ever ".." or empty, this could delete wrong directories.
Validate that suffix is "0" or "1" in both createDs and destroyDs.

* fix(provider): close opened datastores when mounting partially fails

If opening datastore "0" succeeds but "1" fails,
MountKeystoreDatastores returned an error without closing "0".

* fix(provider): defer batch creation in orphan purge until keys are found

Avoids allocating a datastore batch when no orphaned keys exist.

* fix(provider): warn on unrecognized datastore wrapper types

findRootDatastoreSpec silently returns wrapper specs it doesn't know
about. If a plugin adds a wrapper with a "child" field, openDatastoreAt
gets the wrapper instead of the leaf backend and fails confusingly.
Log a warning so operators can spot the issue.

* docs: document keystore migration behavior on upgrade and downgrade

- explain why context.Background() is used in the migration code
- add changelog note about the provide cycle restarting on upgrade
- add downgrade caveat about orphaned provider-keystore directory

* chore(deps): bump go-libp2p-kad-dht to latest keystore factory commit

* fix(provider): harden keystore migration and spec handling

- chunk orphan purge into 4096-key batches to bound memory and
  match existing batching patterns in the same file
- cancel the purge context via fx.Lifecycle OnStop so SIGINT
  during startup does not block indefinitely
- deep-copy slices in copySpec (not just maps) so the function
  matches its documented "deep-copy" contract
- return nil from findRootDatastoreSpec when no "/" mount exists,
  so callers fall back to in-memory instead of passing a mount-type
  spec to openDatastoreAt
- rename local variable to avoid shadowing the mount package import

* test(provider): add migration purge test and diag datastore put command

- add `ipfs diag datastore put` subcommand for writing arbitrary
  key-value pairs to the datastore (offline, experimental)
- add DatastorePut harness helper for CLI tests
- add TestProviderKeystoreMigrationPurge: seeds orphaned keystore
  keys via `put`, starts the daemon to trigger migration, verifies
  the orphaned keys are purged and provider-keystore/ dir is created
- add put/get roundtrip test for diag datastore

* chore(deps): bump go-libp2p-kad-dht to 1bede74b8246

* fix(provider): log keystore datastore create and destroy operations

* docs: rewrite provider keystore changelog to focus on user impact

* bump kad-dht@master

---------

Co-authored-by: Marcin Rataj <lidel@lidel.org>
* feat(cmd): add 'ipfs cid inspect' command

Adds a new subcommand to inspect and display detailed CID information
including version, multibase encoding, multicodec, and multihash
components. Also shows equivalent CIDv0/CIDv1 representations.

Example output:
  CID:        bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi
  Version:    1
  Multibase:  base32 (b)
  Multicodec: dag-pb (0x70)
  Multihash:  sha2-256 (0x12)
    Length:   32 bytes
    Digest:   c3c4733ec8affd06cf9e9ff50ffc6bcd2ec85a6170004bb709669c31de94391a
  CIDv0:      QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR
  CIDv1:      bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi

Supports --enc=json for machine-readable output.

Fixes #11205

* refactor: tidy up CidInspectRes struct and add '/cid/inspect' route to tests

This commit refines the CidInspectRes struct for better readability and consistency. Additionally, it includes the '/cid/inspect' route in the command tests to ensure comprehensive coverage of the new CID inspection functionality.

* docs: update changelog for v0.42 to include new `ipfs cid inspect` command

Added a section highlighting the new `ipfs cid inspect <cid>` command, detailing its functionality to display comprehensive CID information, including version, encoding, and hash details. The command supports machine-readable output and operates offline.

* feat(cmd): improve ipfs cid inspect

- multibase: always shown (implicit for CIDv0), prefix as string
- multicodec/multihash: annotated (implicit) for CIDv0
- digest: uppercase hex with 0x prefix
- cidV0: empty in JSON when not possible, text encoder explains why
- cidV1: base36 for libp2p-key codec, base32 otherwise
- errors: ErrorMsg kept for HTTP RPC API, text encoder returns non-zero exit
- PeerID fallback: helpful hint with equivalent CID on invalid input
- unknown codec/hash: graceful "unknown" label
- stdin support via .EnableStdin()
- inspect listed first in subcommands, cid format points to inspect
- cli tests for all cases including JSON, PeerID, unknown codec

* chore: move cid inspect changelog to v0.41

- digest: bare lowercase hex (no 0x prefix), matching sha256sum

---------

Co-authored-by: Marcin Rataj <lidel@lidel.org>
`ipfs key export` was using `os.Create` (0o666 pre-umask, typically
0o644) making exported private keys world-readable on multi-user
systems. Use `os.OpenFile` with 0o600 to match the restrictive
permissions the keystore itself uses for key files.
* fix(object): validate UnixFS type in patch add-link

Reject adding named links to non-directory nodes in `object patch
add-link`, which previously produced invalid DAGs silently.

- reject UnixFS File/Symlink/etc nodes (only Directory and HAMTShard
  support named links per the UnixFS spec)
- reject non-UnixFS dag-pb nodes (no UnixFS metadata to validate)
- add `--allow-non-unixfs` flag to bypass both checks
- pass `allow-non-unixfs` in client/rpc when SkipUnixFSValidation is set
- test all three node types: bare dag-pb, UnixFS File, UnixFS Directory
- reproduce the exact data-loss scenario from #7190

Fixes: #7190

* fix(object): reject HAMTShard in patch add-link

dagutils.Editor operates at the dag-pb level and does not update
HAMT bitfields, so mutating HAMTShard nodes produces corrupt DAGs.

- reject HAMTShard in add-link (was incorrectly allowed)
- update help text to note dag-pb limitations and suggest ipfs files
- add HAMT test cases to sharness and API tests
- expect full error strings in all validation tests
- update changelog to cover all rejected node types

* fix(object): validate UnixFS type in patch rm-link

Same issue as add-link: dagutils.Editor operates at the dag-pb level
and cannot update UnixFS metadata, so mutating non-Directory nodes
produces corrupt DAGs.

- add UnixFS validation to rm-link (Directory allowed, all else rejected)
- add --allow-non-unixfs flag to rm-link command
- add ObjectRmLinkSettings/ObjectRmLinkOption types
- update ObjectAPI.RmLink interface to accept options
- pass allow-non-unixfs in client/rpc
- update rm-link help text to note dag-pb limitations
- add rm-link validation tests for all four node types
Use request-scoped otelhttp.Labeler instead, which is the
recommended replacement per upstream otelhttp v0.67.0.

- add withMetricLabels helper wrapping inner handler
- migrate all 4 call sites in commands.go and gateway.go
* test(cli): add CARv2 import over HTTP API test

Regression test for #9361.
Imports a CARv2 fixture via the daemon (online mode) and verifies
the blocks are accessible. Currently fails with "operation not
supported" due to the multipart reader not supporting seeking.

* fix(cmd): support CARv2 import over HTTP API

Strip the io.Seeker interface from the file before passing it to
go-car's NewBlockReader. Over the HTTP API the underlying reader is
a multipart stream that cannot seek, but boxo's ReaderFile advertises
io.Seeker and returns ErrNotSupported at runtime. Hiding the interface
lets go-car fall back to forward-only reading.

Fixes #9361

---------

Co-authored-by: Andrew Gillis <11790789+gammazero@users.noreply.github.com>
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.5.2 to 5.5.3.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](codecov/codecov-action@671740a...1af5884)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 5.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Andrew Gillis <11790789+gammazero@users.noreply.github.com>
)

* fix(MFS): fix deadlock, attrs, caching

* unmount ipns and mfs in mount tests; allow offline

* set attrs Uid, Gid, and Valid for readonly and /ipns

* doc: update changelog

* fix(fuse): maximize kernel cache for immutable /ipfs paths

/ipfs content is addressed by CID and never changes, so kernel
attribute caching is safe and avoids unnecessary FUSE round-trips.
Also sets uid/gid on Root.Attr for consistency.

* docs: move FUSE changelog to v0.41 highlights

* fix(fuse): make IPNS fsync a no-op

Calling fsync on a file opened through /ipns deadlocks and eventually
panics, taking down the entire IPNS mount.

The Fsync handler called mfs.File.Flush(), which tries to open a
second write descriptor on the same file. Only one write descriptor
can exist at a time (desclock is exclusive), and the first one from
Open is still held. The new one blocks forever waiting for the lock.
After the FUSE timeout, Release tries to close the original descriptor
and hits a nil pointer panic in DagModifier.Sync.

Make Fsync a no-op, matching the MFS mount. Data gets flushed when
the file is closed. Also improve the MFS Fsync comment to explain
the same constraint.

* fix(fuse): set uid/gid on IPNS symlinks

The "local" symlink in /ipns showed uid=0 gid=0 (root) while
directories and files showed the daemon's uid/gid. Set uid/gid
and disable attr caching to match other mutable IPNS nodes.

Also add TODO comments across all three FUSE mounts for using
Mode and Mtime from UnixFS records when present, and for wiring
IPNS record TTL into attr cache duration.

* fix(fuse): return empty listing for empty directories

IPNS Directory.ReadDirAll and readonly Node.ReadDirAll returned
ENOENT when a directory had no children. An empty directory still
exists, it just has nothing in it. Return an empty slice instead.

MFS already handles this correctly. The readonly Root.ReadDirAll
correctly returns EPERM (you can't list all of /ipfs). The IPNS
Root.ReadDirAll always has entries (peer keys), so it was never
affected.

This matters for /ipfs because empty directories are valid
content-addressed objects (e.g. QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn
is a well-known CID of an empty UnixFS directory).

* fix(fuse): always sync MFS writes to root on close

The Sync flag was computed as `req.Flags|fuse.OpenSync > 0` (bitwise
OR), which is always true because fuse.OpenSync is non-zero. Replace
with an explicit `true` to match the IPNS mount and make the intent
clear: FUSE writes must always propagate to the MFS root when the
file is closed, regardless of whether the caller set O_SYNC.

* docs: update FUSE changelog for new fixes

* test(fuse): add empty directory listing tests

Verify that listing an empty directory returns an empty result
instead of an error, for all three FUSE mounts:

- /mfs: empty root + empty subdirectory
- /ipns: empty peer directory + empty subdirectory
- /ipfs: empty UnixFS directory added to the DAG

* test(fuse): add append and byte-at-a-time write tests for MFS

IPNS had TestAppendFile and TestMultiWrite but MFS did not. Add
matching tests to cover appending to an existing file and writing
one byte at a time.

* ci(fuse): add dedicated FUSE test job with auto-detection

Add a fuse-tests CI job that installs fuse3, sets TEST_FUSE=1, and
runs FUSE unit tests. Previously these tests were compiled out by
the nofuse build tag (set when TEST_FUSE=0 in the unit-tests job).

Introduce fuse/fusetest package with shared test helpers:

- SkipUnlessFUSE: respects TEST_FUSE env var (0=skip, 1=run) with
  auto-detection fallback that checks for fusermount in PATH
- MountError: fatals when TEST_FUSE=1 (CI expects FUSE to work),
  skips when auto-detecting (local dev without FUSE)

Replace the old ci.NoFuse() (checked TEST_NO_FUSE, a dead env var
nobody set) and per-file maybeSkipFuseTests wrappers.

On Linux, bazil.org/fuse hardcodes "fusermount" but modern distros
only ship "fusermount3". The CI job creates a symlink; the
auto-detect gives a helpful skip message when only fusermount3 is
found locally.

* fix(fuse): handle EINTR on close in IPNS concurrent write test

TestConcurrentWrites was flaky because Go's goroutine preemption
signal (SIGURG) can interrupt the FUSE FLUSH inside close(),
returning EINTR. The write itself already succeeded and the kernel
will still send RELEASE to the daemon, so the data is safe.

Replace os.WriteFile with explicit open/write/close so we can
ignore EINTR on close while still catching real errors.

* fix(fuse): resolve bare file CIDs on /ipfs mount

Accessing a file by its CID at the /ipfs FUSE mount root returned
ENOENT because ProtoNodeConverter cannot handle UnixFS file ADLs.
Decode dag-pb blocks directly from bytes instead.

Closes #9044

* fix(fuse): fix same-directory rename on /mfs

Renaming a file within the same MFS directory left the source behind.
The directory's entry cache was re-synced before the old name was
removed. Unlink the source before AddChild to match the working
IPNS pattern.

* test(fuse): add mixed dag-pb/raw directory test

Covers the scenario from #9044:
a directory with both dag-pb and raw-leaf children read through
the /ipfs FUSE mount.

* test(fuse): remove redundant testing.Short() checks

SkipUnlessFUSE(t) already handles skipping via TEST_FUSE.
The testing.Short() guard was a second skip gate that served
no purpose since FUSE tests only run under make test_fuse.

* fix(fuse): get DAG node before unlinking source in rename

Move GetNode() before Unlink() in both mfs and ipns Rename
so that a GetNode() failure does not leave the source entry
already removed. Also add FUSE test instructions to AGENTS.md.

* ci: skip fuse3 install when fusermount exists

Self-hosted runners persist state, so after the first run
fuse3 and the symlink are already in place. Skip apt-get
update and install entirely when fusermount is in PATH.

---------

Co-authored-by: Andrew Gillis <11790789+gammazero@users.noreply.github.com>
Co-authored-by: Marcin Rataj <lidel@lidel.org>
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.5.3 to 6.0.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](codecov/codecov-action@1af5884...57e3a13)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(fuse): persist IPNS writes across restarts

The IPNS FUSE mount's MFS republisher calls Name.Publish to persist
changes, but checkPublishAllowed blocks all publishes while the mount
is active. This means writes through the FUSE mount are silently
dropped and lost on daemon restart.

Add a context key so the mount's internal publishes bypass the guard
while manual `ipfs name publish` from CLI/RPC remains blocked.

- core/coreiface/name.go: context key and helpers for mount publish
- core/coreapi: checkPublishAllowed checks context before blocking
- fuse/ipns: ipnsPubFunc marks its context as mount-internal
- fuse/ipns: tests set node.Mounts.Ipns to exercise the guard

Fixes #2168

* docs: add IPNS FUSE persistence fix to v0.41 changelog

* refactor(fuse): move publish bypass to internal package

Move the FUSE mount publish context key from the public coreiface
package to internal/fusemount, preventing external consumers from
bypassing the publish guard.

- internal/fusemount/context.go: new internal package with context helpers
- core/coreiface/name.go: remove exported ContextWithMountPublish / IsMountPublish
- core/coreapi/coreapi.go: use fusemount.IsPublish for the guard check
- fuse/ipns/ipns_unix.go: use fusemount.ContextWithPublish to tag context
…#11265)

* chore(deps): bump github.com/hashicorp/go-version from 1.8.0 to 1.9.0

Bumps [github.com/hashicorp/go-version](https://github.com/hashicorp/go-version) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/hashicorp/go-version/releases)
- [Changelog](https://github.com/hashicorp/go-version/blob/main/CHANGELOG.md)
- [Commits](hashicorp/go-version@v1.8.0...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/go-version
  dependency-version: 1.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: run make mod_tidy

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andrew Gillis <11790789+gammazero@users.noreply.github.com>
* chore(deps): bump the ipfs-ecosystem group with 2 updates

Bumps the ipfs-ecosystem group with 2 updates: [github.com/ipfs/go-ds-pebble](https://github.com/ipfs/go-ds-pebble) and [github.com/ipfs/go-test](https://github.com/ipfs/go-test).


Updates `github.com/ipfs/go-ds-pebble` from 0.5.9 to 0.5.10
- [Release notes](https://github.com/ipfs/go-ds-pebble/releases)
- [Commits](ipfs/go-ds-pebble@v0.5.9...v0.5.10)

Updates `github.com/ipfs/go-test` from 0.2.3 to 0.3.0
- [Release notes](https://github.com/ipfs/go-test/releases)
- [Commits](ipfs/go-test@v0.2.3...v0.3.0)

---
updated-dependencies:
- dependency-name: github.com/ipfs/go-ds-pebble
  dependency-version: 0.5.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: ipfs-ecosystem
- dependency-name: github.com/ipfs/go-test
  dependency-version: 0.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: ipfs-ecosystem
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: run make mod_tidy

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Andrew Gillis <11790789+gammazero@users.noreply.github.com>
* test(deps): quick test of boxo with ipfs/boxo#1125

* test(mfs): verify CidBuilder preservation across mutations and restarts

* docs(changelog): highlight MFS CidBuilder fix

* fix(mfs): apply Import.CidVersion and HashFunction to MFS root

The MFS root loaded at daemon startup never received a CidBuilder
from config, so it stayed CIDv0/sha2-256 even with non-default
Import settings. Pass the configured CidBuilder to mfs.NewRoot().

- add Import.UnixFSCidBuilder() helper for building cid.Prefix
- pass WithCidBuilder to mfs.NewRoot in core/node/core.go
- deduplicate getPrefixNew/getPrefix in files.go
- strengthen regression test to check CID version and root dir

* fix: restore explicit Flush, use upstream boxo

- restore explicit Flush: false in addNode and addDir Mkdir calls
  that was dropped during the MkdirOpts refactor
- use %q for hash function error message in getPrefix
- switch from boxo fork replace to upstream boxo@98dabcc

* fix(config): always build explicit CidBuilder from defaults

UnixFSCidBuilder used to return nil when CidVersion and HashFunction
matched compile-time defaults, relying on boxo's internal CIDv0/sha2-256
fallback. This will break when DefaultCidVersion changes to 1, because
boxo will keep using CIDv0 regardless.

- remove early-return short-circuit in UnixFSCidBuilder
- add unit tests for explicit and default CidBuilder construction

Refs: #4143

* fix(files): reject chcid on MFS root path

The MFS root CID format is now always set from Import.CidVersion and
Import.HashFunction at startup, so chcid on "/" was silently overridden
on every subsequent command or daemon restart.

- chcid now requires a path argument and rejects "/"
- help text and changelog explain how to change root CID format
- sharness tests use Import config + daemon restarts instead of chcid /
- added test for chcid on subdirectories with blake2b-256

* chore(deps): update boxo to latest main

Picks up ipfs/boxo#1131: fix concurrent flush/close panic in MFS
file descriptors (FUSE race condition).

* fix(test): sharness daemon pairing and stale shard hash

- add restart_daemon helper to avoid tripping t0015 meta-test
  that counts literal test_kill/test_launch pairs in each file
- update cidv1 SHARD_HASH to match current boxo HAMT output

---------

Co-authored-by: Andrew Gillis <11790789+gammazero@users.noreply.github.com>
* feat: go 1.26.2, once more with feeling

go 1.26 was reverted in v0.40.1 due to a windows crash in
go's overlapped i/o layer. upstream fix landed in go 1.26.2,
so kubo can switch back across all platforms.

- go.mod, kubo-as-a-library, test/dependencies: go 1.26.2
- Dockerfile: golang:1.26.2 (pinned to same patch as go.mod)
- v0.41 changelog: restore go 1.26 highlight, link upstream fix

ref: #11214
ref: golang/go#78041

* ci: parse go version from go.mod for docker builds

Dockerfile FROM lines must be literals, so go.mod can't drive them
directly. The docker workflows now parse the `go` directive from
go.mod and pass it as a GO_VERSION build arg, making go.mod the
single source of truth across setup-go and docker. Local
`docker build .` still works via the ARG default, and a lint check
fails if that default drifts.

- Dockerfile: ARG GO_VERSION=1.26.2 before FROM golang:${GO_VERSION}
- docker-image.yml: read go.mod, pass GO_VERSION to all 4 build steps
- docker-check.yml: same for the build job; lint job verifies the
  Dockerfile ARG default matches the go directive in go.mod
* Upgrade to Boxo v0.38.0
* test(fuse): consolidate FUSE tests into test/cli/fuse

Move FUSE integration tests from sharness shell scripts (t0030, t0031,
t0032) and test/cli/fuse_test.go into a dedicated test/cli/fuse/ Go
sub-package, ensuring all FUSE test cases run in CI.

- git mv test/cli/fuse_test.go to test/cli/fuse/ (package fuse)
- convert all sharness FUSE tests to Go subtests under TestFUSE:
  mount failure, IPNS symlink, IPNS NS map resolution, MFS file/dir
  creation, xattr (Linux), files write, add --to-files, file removal,
  nested dirs, publish-while-mounted block, sharded directory reads
- add xattr helpers with build tags (linux/other) using unix.Getxattr
- split make test_fuse into test_fuse_unit (./fuse/...) and
  test_fuse_cli (./test/cli/fuse/...) sub-targets
- set TEST_FUSE=0 in test_cli so FUSE tests skip in cli-tests CI job
- increase fuse-tests CI timeout from 5m to 10m for CLI tests
- delete sharness t0030, t0031, t0032 (were always skipped in CI)

* docs: document FUSE test split between unit and e2e

Add cross-reference comments between the unit tests in fuse/readonly/,
fuse/ipns/, fuse/mfs/ and the end-to-end CLI tests in test/cli/fuse/.
Also fix AGENTS.md to use a temp dir for fusermount symlink instead of
sudo.

* ci: prevent stale FUSE mounts from failing fuse-tests

On shared self-hosted runners, leftover mount points from previous
runs can exhaust the kernel FUSE mount limit.

- add job-level concurrency group so only one fuse-tests runs at a time
- lazy-unmount stale /tmp/fusetest* mounts before running tests

* ci: only symlink fusermount3 when fusermount is missing

* fix(fuse): remove goroutine leak in IPNS Flush handler

The Flush handler wrapped fi.fi.Flush() in a goroutine so it could
return early when the FUSE context was canceled. But the goroutine
kept running in the background, and when Release arrived it called
Close on the same file descriptor concurrently. The two paths both
entered DagModifier.Sync, racing on its internal write buffer and
causing a nil pointer panic.

The fix is to call Flush directly without a goroutine. The MFS flush
cannot be safely canceled mid-operation anyway, so the goroutine
only added the illusion of cancellation while leaking work and
masking the real error.

Also bumps boxo to pick up the matching defense-in-depth fix that
serializes FileDescriptor.Flush and Close with a mutex.

* fix(fuse): add mutex to IPNS file handle operations

bazil/fuse dispatches each FUSE request in its own goroutine.
The IPNS File handle had no synchronization, so concurrent
Read/Write/Flush/Release calls could overlap on the underlying
DagModifier which is not safe for concurrent use.

Add sync.Mutex to File, matching the pattern already used by the
MFS FileHandler.

* refactor(fuse): remove dead File.Forget method

bazil/fuse only dispatches Forget to nodes via the NodeForgetter
interface. File is a handle, not a node, so this method was never
called. The /mfs mount has no equivalent.

* fix(fuse): flush IPNS directory after Remove and Rename

The /mfs mount flushes the directory after Unlink and Rename so
changes propagate to the MFS root immediately. The /ipns mount
did not, leaving mutations pending until an unrelated flush.

Also add an empty-directory check before removing directories,
matching the /mfs mount's safety check.

* fix(fuse): inherit CID builder and flush on IPNS Create

New files created via the /ipns FUSE mount now inherit the CID
builder from their parent directory, preventing CIDv0 nodes from
appearing inside a CIDv1 tree.

The directory is also flushed after AddChild so the new entry
propagates to the MFS root immediately, matching the /mfs mount.

* test(fuse): add IPNS Remove and non-empty rmdir tests

Cover the file removal path and the empty-directory safety check
added in the previous commit. TestRemoveFile verifies a created
file can be removed and is gone afterwards. TestRemoveNonEmptyDirectory
verifies that rmdir on a directory with children fails, and succeeds
once the children are removed first.

* feat(fuse): read UnixFS mode/mtime, add StoreMtime/StoreMode config

All three FUSE mounts now read mode and mtime from UnixFS metadata
when present, falling back to POSIX defaults when absent. Most IPFS
data does not include this optional metadata.

Writing mode and mtime is opt-in via two new config flags:
- Mounts.StoreMtime: persist mtime on file create and open-for-write
- Mounts.StoreMode: persist mode on chmod

Other changes in this commit:
- align default file/dir modes across /ipns and /mfs to 0644/0755
- share mode constants via fuse/mount/mode.go
- convert Mounts.FuseAllowOther from bool to Flag for consistency
- add Setattr to /ipns FileNode and /mfs File for chmod and touch
- move dead File.Setattr from IPNS handle to FileNode (node)
- bump boxo for Directory.Mode() and Directory.ModTime() getters

* feat(fuse): add ipfs.cid xattr to all mounts

All three FUSE mounts now expose the node's CID via the ipfs.cid
extended attribute on both files and directories.

The /mfs mount also accepts the old ipfs_cid name for backward
compatibility. The /ipfs mount previously had a stub that returned
nil for all xattrs; it now returns the correct CID.

The xattr name follows the convention used by CephFS (ceph.*),
Btrfs (btrfs.*), and GlusterFS (glusterfs.*).

* feat(fuse): switch from bazil.org/fuse to hanwen/go-fuse v2

Replace the unmaintained bazil.org/fuse (last commit 2020) with
hanwen/go-fuse v2.9.0, fixing two architectural issues that could
not be solved with the old library.

ftruncate now works: hanwen/go-fuse passes the open file handle to
NodeSetattrer, so Setattr can truncate through the existing write
descriptor instead of trying to open a second one (which deadlocks
on MFS's single-writer lock).

fsync now works: FileFsyncer runs on the handle directly, flushing
the write buffer through the open descriptor. Previously a no-op
because bazil dispatched Fsync to the inode only.

mount package:
- NewMount takes (InodeEmbedder, mountpoint, *fs.Options) instead
  of (fs.FS, mountpoint, allowOther)
- mount/unmount collapses to a single fs.Mount call
- fusermount3 tried before fusermount in ForceUnmount

all three mounts:
- structs embed fs.Inode (hanwen's InodeEmbedder pattern)
- Remove split into Unlink + Rmdir (separate FUSE interfaces)
- ReadDirAll replaced with Readdir returning DirStream
- fillAttr helper shared between Getattr and Lookup responses
- kernel cache invalidation via NotifyContent after Flush
- 1s entry/attr timeout for writable mounts (matches go-fuse
  default, gocryptfs, rclone)
- O_APPEND tracked on file handle, writes seek to end
- build tags standardized to (linux || darwin || freebsd) && !nofuse

tests:
- replaced bazil fstestutil.MountedT with shared fusetest.TestMount
- fixed TestConcurrentRW: channel drain mismatch and missing sync
  between write Close and read start
- added TestFsync, TestFtruncate, TestReadlink, TestSeekRead,
  TestLargeFile, TestRmdir, TestCrossDirRename, TestUnknownXattr
- added StoreMtime disabled/enabled subtests

* fix(fuse): close fd on error in Open to prevent leak

MFS enforces a single-writer lock, so a leaked write descriptor
blocks all subsequent opens of that file until GC.

* fix(fuse): detect external unmount via server.Wait

Without this, IsActive stays true after `fusermount -u` and
Unmount returns nil instead of ErrNotMounted.

* fix(fuse): return actual error from Unlink/Rmdir, not ENOENT

After confirming the child exists, an Unlink failure could be an
IO error. Returning ENOENT would hide the real cause.

* fix(fuse): reuse DagReader per open, pass ctx to all reads

Readonly Open now returns a file handle holding a DagReader instead
of recreating one per Read call. Sequential reads no longer
re-traverse the DAG from the root on each kernel request.

All three mounts now use CtxReadFull with the kernel's per-request
context so killing a process mid-read cancels in-flight block
fetches instead of letting them complete uselessly.

* chore(fuse): cleanup dead code, add var comments

- remove dead `_ = mntDir` in TestXattrCID
- comment why immutableAttrCacheTime and mutableCacheTime are var
- add TODO for using IPNS record TTL as cache timeout

* chore(fuse): replace OSXFUSE 2.x check with macFUSE detection

The old check tried to verify OSXFUSE >= 2.7.2 to avoid a kernel
panic from 2015. It used sysctl, tried to `go install` a third-party
tool at runtime, and referenced paths that no longer exist.

Replace with a simple check for the macFUSE mount helper, matching
the same paths go-fuse looks for. If neither macFUSE nor OSXFUSE is
found, point the user to the install page.

Also standardize build tags to (linux || darwin || freebsd) && !nofuse
and use strings.ReplaceAll.

* fix(fuse): include mountpoint path in mount errors

go-fuse's fusermount errors don't include the path, so tools that
check error messages for the mountpoint name couldn't tell which
mount failed.

* chore(ci): remove bazil fusermount workaround

go-fuse finds fusermount3 natively, no symlink needed. The stale
mount cleanup was for bazil's fstestutil which we no longer use.

* docs: update v0.41 changelog for FUSE rewrite

* chore(deps): bump boxo for full FileDescriptor serialization

boxo@64be0815 extends the mutex from Flush/Close to all
FileDescriptor operations (Read, Write, Seek, Truncate, Size),
preventing data races on the underlying DagModifier.

* chore(deps): bump boxo to merged ipfs/boxo#1133

Picks up full FileDescriptor serialization: the mutex now covers
all operations (Read, Write, Seek, Truncate, Size), not just
Flush and Close.

* feat(fuse): CAP_ATOMIC_O_TRUNC, new integration tests

Advertise CAP_ATOMIC_O_TRUNC so the kernel sends O_TRUNC inside
Open instead of doing a separate SETATTR(size=0) first. Without
this, the kernel's SETATTR needs to open a write descriptor inside
Setattr, which deadlocks on MFS's single-writer lock.

Move kernel cache invalidation from Flush to Release because
mfsFD.Close (in Release) is where the final DAG node is committed.

Upgrade go-fuse to latest for ExtraCapabilities support.

New tests for both MFS and IPNS:
- TestOpenTrunc, TestSeekAndWrite, TestOverwriteExisting
- TestTempFileRename, TestVimSavePattern, TestRsyncPattern
  (skipped pending rename-over-existing and cache fixes)

* fix(fuse): rename-over-existing, bump boxo for flushUp race fix

IPNS Rename now unlinks the target before AddChild, matching MFS.
Without this, renaming onto an existing name returned "directory
already has entry".

Bump boxo to pick up the flushUp unlinked-entry fix (ipfs/boxo@8ae46d5):
when a file descriptor outlives its directory entry (FUSE RELEASE
racing with RENAME), flushUp no longer re-adds the stale name.

Unskip TestTempFileRename and TestRsyncPattern on both mounts.

* fix(fuse): unskip VimSavePattern, bump boxo for setNodeData fix

boxo@552d8e7 fixes File.setNodeData dropping content links when
updating metadata (mode, mtime). chmod or touch after write no
longer makes the file appear empty.

Unskip TestVimSavePattern on both mounts. Remove debug logging
and temporary test functions added during investigation.

* fix(fuse): build tags for cross-compilation

go-fuse does not compile on windows/openbsd/netbsd/plan9. Move
WritableMountCapabilities (which imports go-fuse) from mode.go
(no build tag) to caps.go (platform-gated). Align build tags on
fusetest and core/commands/mount stubs so unsupported platforms
don't pull in go-fuse transitively.

* fix(test): use fusermount3 in CLI FUSE tests

The doUnmount helper hardcoded fusermount, but systems with only
fuse3 installed have fusermount3. Try fusermount3 first, matching
what go-fuse and our ForceUnmount already do.

* feat(fuse): symlink support on writable mounts

Add NodeSymlinker to MFS and IPNS directories. Symlinks are stored
as UnixFS TSymlink nodes in the DAG, the same format used by
`ipfs add` for directories containing symlinks. The readonly /ipfs
mount already rendered existing symlinks; now /mfs and /ipns can
create them too.

The target string is cached at Lookup time to avoid re-parsing the
DAG node on every Readlink call. Symlink permissions are always
0777 per POSIX convention (access control uses the target's mode).

* fix(fuse): checked type assertion in MFS Rename

The direct type assertion on newParent could panic if the kernel
passed a non-directory inode. Use a checked assertion with EINVAL
fallback, matching the type-switch pattern in the IPNS mount.

* fix(test): add missing continue in stress test

Missing continue after error sends let execution fall through to
nil type assertions (read.(files.File)) that would panic on error.
Also cancel the context before continuing to avoid leaking it.

* fix(fuse): return error from Readdir when DAG.Get fails

Abort the directory listing instead of silently omitting the
unretrievable entry. Callers get EIO, which is more honest than
a partial listing that hides missing blocks.

* docs: remove duplicate fsync bullet in changelog

* ci: clean up stale FUSE mounts in fuse-tests job

On shared self-hosted runners, leftover mounts from crashed runs
can exhaust the kernel mount_max limit. Lazy-unmount kubo-test
and harness temp mounts before and after tests.

* chore(deps): bump boxo to merged ipfs/boxo#1134

Picks up flushUp unlinked-entry guard and setNodeData content link
preservation.

* docs: add build tag comments, normalize tag style

Add a one-line comment above every //go:build directive explaining
why the constraint exists.

Normalize tag style: positive platform constraints first, then
feature flags/negations. Simplify redundant expressions.

* fix(fuse): add Setattr to directories for chmod and mtime

Tools like tar and rsync call utimensat on directories after
extraction. Without Setattr on Dir, this returned ENOTSUP.

Add Setattr to Dir (MFS) and Directory (IPNS) that handles mode
and mtime the same way as the file-level Setattr. When StoreMtime
or StoreMode is disabled the call succeeds silently, matching the
file-level behavior.

* docs: clarify directory support and spec link for StoreMtime/StoreMode

- mention that touch and chmod work on both files and directories
- note tar and rsync as practical use cases
- link to UnixFS spec for optional metadata storage

* fix(fuse): use proper mode conversion, document 9-bit limit

Use files.UnixPermsToModePerms and files.ModePermsToUnixPerms for
converting between FUSE kernel mode (unix 12-bit layout) and Go's
os.FileMode (different bit positions for setuid/setgid/sticky).

The UnixFS spec supports all 12 permission bits, but boxo's MFS
layer (File.Mode, Directory.Mode) exposes only the lower 9. FUSE
mounts are always nosuid so the upper 3 bits would have no effect.

Add TestSetuidBitsStripped to both mounts confirming the behavior.

* feat(fuse): symlink Setattr with mtime persistence

Wire the backing mfs.File into the FUSE Symlink struct so Setattr
can call SetModTime when StoreMtime is enabled. boxo's File methods
(SetModTime, ModTime) already work on TSymlink nodes since they
operate on the FSNode protobuf without checking the type.

Without Setattr, rsync -a fails with "failed to set times" on
symlinks. Every major FUSE filesystem (gocryptfs, rclone, sshfs,
s3fs) implements Setattr on symlinks for this reason.

Mode is always 0777 per POSIX convention, so chmod requests are
silently accepted but not stored.

* fix(fuse): return EIO instead of panicking on unknown node type

Replace panic with log.Errorf + syscall.EIO in IPNS Directory.Lookup
for unexpected MFS node types. Also remove duplicate comment block
on File.Flush.

* docs: update FUSE docs for go-fuse migration

- fuse.md: replace stale OSXFUSE section with macFUSE, remove
  obsolete go-fuse-version tool, fix broken FreeBSD sudo echo,
  update xattr example to ipfs.cid with CIDv1, add mode/mtime
  section, add unixfs-v1-2025 tip, add debug logging section,
  add TOC, link to hanwen/go-fuse
- changelog: refine bullet wording, link to fuse.md
- config.md: fix double space, update fuse.md link text
- experimental-features.md: fix double space, soften wording
- README.md: add FUSE to features list and docs table

* refactor(fuse): extract shared writable types and test suite

Extract duplicated code from fuse/mfs and fuse/ipns into a shared
fuse/writable package, and consolidate duplicated tests into a
reusable suite in fuse/fusetest.

- fuse/writable: Dir, FileInode, FileHandle, Symlink types with all
  FUSE interface methods, shared by both mounts
- fuse/fusetest: RunWritableSuite with helpers, exercised by both
  mfs and ipns via mount-specific factories
- fix cache invalidation race: NotifyContent in Flush (synchronous)
  in addition to Release (async), so stat after close sees new size
- drop deprecated ipfs_cid xattr, log error guiding users to ipfs.cid
- mfs_unix.go: 632 -> 19 lines (thin wrapper over writable.Dir)
- ipns_unix.go: 795 -> 170 lines (Root + key resolution only)
- mfs_test.go: 1183 -> 95 lines (factory + persistence test)
- ipns_test.go: 1309 -> 162 lines (factory + IPNS-specific tests)
- tests that were only in one mount now run on both

* feat(fuse): add macOS-specific mount options

Set volname, noapplexattr, and noappledouble on macOS via
PlatformMountOpts, applied in NewMount so all three mounts
benefit automatically.

- volname: shows mount name in Finder instead of "macfuse Volume 0"
- noapplexattr: suppresses Finder's com.apple.* xattr probes
- noappledouble: prevents ._ resource fork sidecar files

* fix(fuse): detect symlinks in readdir, fix stale refs

Readdir on writable mounts now checks the underlying DAG node type
for TFile entries, reporting S_IFLNK for symlinks instead of regular
file. This makes ls -l and find -type l work correctly.

- writable: Readdir checks SymlinkTarget for TFile entries
- writablesuite: add SymlinkReaddir regression test
- readonly: add TestReaddirSymlink regression test
- test/cli/fuse: fix stale bazil.org/fuse reference in doc comment

* fix(fuse): normalize deprecated ipfs_cid xattr to ipfs.cid

Getxattr for the old "ipfs_cid" name now returns the CID instead of
ENOATTR, keeping existing tooling working during the deprecation
period. A log error is emitted on each access to nudge migration.

* fix(fuse): serialize concurrent reads on readonly file handles

The go-fuse server dispatches each FUSE request in its own goroutine.
On files larger than 128 KB the kernel issues concurrent readahead
Read requests on the same file handle, racing on the shared DagReader's
Seek+CtxReadFull sequence and corrupting its internal state.

Add sync.Mutex to roFileHandle (matching the existing pattern in
writable.FileHandle) and lock in Read and Release.

- fuse/readonly/readonly_unix.go: add mu sync.Mutex to roFileHandle
- fuse/readonly/ipfs_test.go: add TestConcurrentLargeFileRead
- fuse/fusetest/writablesuite.go: add LargeFileConcurrentRead to
  shared writable suite (exercised by both /mfs and /ipns tests)

* fix(fuse): bypass MFS locking for read-only opens

MFS uses an RWMutex (desclock) that holds RLock for the lifetime of a
read descriptor and requires exclusive Lock for writes. Tools like
rsync --inplace open the same file for reading and writing from
separate processes, deadlocking on this mutex.

For O_RDONLY opens, create a DagReader directly from the current DAG
node instead of going through MFS. The reader gets a point-in-time
snapshot and never touches desclock, so writers proceed independently.

- fuse/writable/writable.go: add roFileHandle with DagReader for
  read-only opens, add DAG field to Config
- fuse/mfs/mfs_unix.go: pass ipfs.DAG to writable Config
- fuse/ipns/ipns_unix.go: pass ipfs.Dag() to writable Config
- fuse/fusetest/writablesuite.go: add ConcurrentReadWrite test
  exercising simultaneous read and write on the same file

* fix(fuse): support truncate(path, size) without open fd

Open a temporary write descriptor in Setattr when the kernel sends a
size change without a file handle (the truncate(2) syscall, as opposed
to ftruncate(fd) which passes the handle). Previously this returned
ENOTSUP.

- fuse/writable: open, truncate, flush, close in Setattr else branch
- fuse/fusetest: add TruncatePath to the shared writable suite
- test/cli/fuse: add end-to-end truncation test covering ftruncate(fd),
  syscall.Truncate(path), and open(O_TRUNC) through a real daemon

* ci(fuse): get stack traces on test hangs

The fuse-tests job was being silently cancelled by GitHub at 10min
because Go's per-test timeout (5m) was the same order as the job
timeout, and GOTRACEBACK=single hid the hung goroutines anyway.

- shrink TEST_FUSE_TIMEOUT to 4m so Go's panic fires first
- shrink job timeout-minutes to 6 (normal run is ~3min)
- set GOTRACEBACK=all so the panic dumps every goroutine, not just the timer

* fix(fuse): fill attrs in FileInode.Setattr response

Without this, the kernel could cache zero attrs after a chmod, touch, or
ftruncate until AttrTimeout (1s) expired. Dir.Setattr and Symlink.Setattr
already fill out.Attr; FileInode.Setattr now matches.

* docs(config): clarify Mounts.IPNS writability scope

Only directories backed by keys the node holds are writable. All other
names resolve via IPNS to read-only symlinks into the /ipfs mount.

* fuse: review cleanup for go-fuse migration

Final pass on #11272 addressing review feedback.

- writable: panic in NewDir if Config.DAG is nil. Both call sites
  already supply it, but a nil value silently fell back to the MFS
  path in FileInode.Open, re-introducing the rsync --inplace deadlock
  the read-only fast path was added to fix.
- writable: document Dir.Rename non-atomicity. Source unlink happens
  before destination add, so any failure between the two loses the
  source. An atomic fix requires changes in boxo/mfs.
- writable: add unit test locking in that Symlink.Setattr accepts a
  mode-only request without erroring and does not store the requested
  mode (POSIX symlinks have no meaningful permission bits).
- docs/config: correct StoreMode default modes; the previous text
  listed 0666 for files, which the code never uses.

* docs(config): list StoreMtime and StoreMode in Mounts TOC

* fix(fuse): fill EntryOut attrs in Dir.Create and Dir.Mkdir

Without this, fstat on the file handle returned by Create reports
mode 0 and size 0 for up to AttrTimeout (1s), because the kernel
caches the empty attrs from the Create response. Path-based stat
goes through Lookup which already fills attrs, so the bug only
shows up via fstat. Mirrors the same fix already applied to
FileInode.Setattr.

Dir.Mkdir gets the same fillAttr treatment for consistency, plus
a TODO noting that boxo's mfs.Directory.Mkdir accepts no mode arg
so the caller's mode is dropped on creation.

Adds CreateAttrsImmediate and MkdirAttrsImmediate to the shared
writable suite to guard both paths against future regressions.

* fix(fuse): map context cancellation to EINTR in read paths

When a userspace process is killed mid-read (Ctrl-C, SIGKILL on a
stuck cat) the kernel sends FUSE_INTERRUPT and go-fuse cancels the
per-request context. fs.ToErrno does not recognise context.Canceled
and falls through to "function not implemented", which the kernel
cannot act on. Map context.Canceled and DeadlineExceeded to EINTR
so the syscall is correctly aborted.

- mount/errno.go: new ReadErrno helper used by all context-aware
  read paths in both readonly and writable mounts
- readonly: applied to Node.Open, Node.Readdir, roFileHandle.Read
- writable: applied to FileInode.Open, FileHandle.Read, roFileHandle.Read
- readonly/ipfs_test.go: TestReadCancellationUnblocks guards the
  contract via a blocking DagReader fake; without ReadErrno the
  test reports "function not implemented" instead of EINTR

* test(fuse): add OExcl, DirRename, SparseWrite, FsyncCrossHandle

Coverage gaps in the shared writable suite:

- OExcl: lock files and atomic-create patterns rely on the second
  open with O_CREATE|O_EXCL failing with EEXIST
- DirRename: previously only file rename and cross-dir file rename
  were tested; this exercises Rename on a directory inode
- SparseWrite: WriteAt past the end of an empty file must report
  the correct size and return zeros for the gap
- FsyncCrossHandle: a reader on a fresh fd must see data flushed by
  fsync on the writer fd, not just after close

* test(fuse): cover external unmount on /ipns and /mfs

Previously TestExternalUnmount only exercised /ipfs, leaving the
goroutine that watches fuse.Server.Wait() untested for the other
two mounts. Refactor into a table-driven test that runs the same
fusermount/umount-then-IsActive flow against all three mounts.

Switch to coremock.NewMockNode so the node is online: doMount only
attaches the /ipns mount when node.IsOnline is true, and the table
needs all three populated.

* fix(commands): align 'ipfs mount' output columns

MountCmd's LongDescription has "MFS  mounted at:" with two spaces
so the column lines up with the 4-char "IPFS" and "IPNS" rows above,
but the runtime encoder and the daemon's startup print used a single
space and produced misaligned output.

Bring both runtime sites in line with the help text, and update the
two existing test fixtures (test/cli/fuse and the sharness test-lib
helper that t0040-add-and-cat.sh still uses) to expect the aligned
form.

* fix(fuse): invalidate kernel cache on Fsync

FileHandle.Fsync only flushed the MFS file descriptor and left the
kernel's cached attrs and content for the inode untouched. A fresh
reader on the same path then saw the size cached from the original
Create response (zero), reading zero bytes regardless of how much
the writer had synced.

Mirror the cache invalidation already done in Flush via
inode.NotifyContent(0, 0) so a writer that fsyncs while another
process opens the file (vim then a follow-up cat, IDE then a
language server) sees consistent state.

Sharpen the FsyncCrossHandle assertion to report the size delta on
failure; the bug surfaced as got=0/want=500 only after switching
from bytes.Equal to require.Equal.

* chore(gitignore): ignore test_fuse_unit and test_fuse_cli json output

The new test_fuse_unit and test_fuse_cli make targets emit
test/fuse/fuse-unit-tests.json and test/fuse/fuse-cli-tests.json
respectively, the same gotestsum --jsonfile pattern that test_unit
and test_cli already use. Add them to the same .gitignore section
so a local test run does not leave the working tree dirty.

* test(fuse): end-to-end coverage with real POSIX tools

Adds TestFUSERealWorld in test/cli/fuse/realworld_test.go: a single
shared-daemon test with 18 subtests that exercise the writable /mfs
mount through the actual binaries users invoke (sh, cat, seq, wc,
ls, stat, cp, mv, rm, ln, readlink, find, dd, sha256sum, tar,
rsync, vim). Each subtest verifies the result both via the FUSE
filesystem and via 'ipfs files read|stat|ls' so both views agree.

Synthetic payloads default to 1 MiB + 1 byte so multi-chunk
read/write paths are exercised, not just single-chunk fast paths.

External tools are required, not optional: a missing binary fails
the test loudly so a CI image change cannot silently turn the suite
green. The whole-suite TEST_FUSE gate is the only place a developer
is allowed to skip.

runCmd forces LC_ALL=C so locale-sensitive tool output (date
formats in 'ls -l', decimal separators in 'wc', localized error
messages, find/ls collation) is deterministic regardless of the
runner's locale settings.

One shared daemon across all 18 subtests keeps total runtime under
two seconds; isolation comes from per-subtest subdirectories under
the mount.
* fix(config): harden provide strategy parsing with error returns

- config: ParseProvideStrategy returns error, rejects "all" mixed with
  selective strategies, removes dead strategy==0 check
- config: add MustParseProvideStrategy for pre-validated call sites
- config: ValidateProvideConfig validates strategy at startup
- config: ShouldProvideForStrategy uses bitmask check for ProvideStrategyAll
- core/node: downstream callers use MustParseProvideStrategy
- core/node: fix Pinning() nil return that caused fx.Provide panic

* feat(config): add +unique and +entities strategy modifiers

- ProvideStrategyUnique: bloom filter cross-DAG deduplication
- ProvideStrategyEntities: entity-aware traversal (implies Unique)
- parser: "unique" and "entities" tokens recognized
- validation: modifiers must combine with pinned/mfs, incompatible
  with all/roots
- go.mod: update boxo to feat/provide-entity-roots-with-dedup
  (VisitedTracker, WalkDAG, WalkEntityRoots, NewConcatProvider,
  NewUniquePinnedProvider, NewPinnedEntityRootsProvider)

* refactor(cmd): rename ExecuteFastProvide to ExecuteFastProvideRoot

pure rename, no behavior change. prepares for ExecuteFastProvideDAG
which will walk the DAG according to Provide.Strategy.

* feat(pin): fast-provide root CID after pin add and pin update

adds ExecuteFastProvideRoot calls to pin add and pin update,
matching the behavior of ipfs add and ipfs dag import. respects
Import.FastProvideRoot and Import.FastProvideWait config options.

previously, pin add/update did not trigger any immediate providing,
leaving pinned content invisible to the DHT until the next reprovide
cycle (up to 22h).

* feat(provider): wire +unique reprovide cycle with bloom dedup

when Provide.Strategy includes +unique, the reprovide cycle uses a
shared BloomTracker across all sub-walks (MFS, recursive pins, direct
pins). duplicate sub-DAG branches across recursive pins are detected
and skipped, reducing traversal from O(pins * total_blocks) to
O(unique_blocks).

- readLastUniqueCount / persistUniqueCount: persist bloom sizing count
  between cycles at /reprovideLastUniqueCount
- uniqueMFSProvider: MFS walker with shared tracker + locality check
- createKeyProvider restructured: +unique bit checked first, non-unique
  strategies fall through to existing switch unchanged
- per-cycle fresh BloomTracker sized from previous cycle's count
- channel wrapper persists count on successful cycle completion

* feat(provider): wire +entities reprovide cycle with entity root walkers

when Provide.Strategy includes +entities (which implies +unique), the
reprovide cycle uses WalkEntityRoots instead of WalkDAG, emitting only
entity roots (files, directories, HAMT shards) and skipping internal
file chunks.

- mfsEntityRootsProvider: MFS walk with entity root detection
- createKeyProvider: select walker based on +entities flag via function
  references (makePinProv / makeMFSProv) to avoid duplicating the
  stream wiring logic
- all combinations: pinned+entities, mfs+entities, pinned+mfs+entities

* docs: document +unique and +entities strategy modifiers

- config.md: document +unique, +entities modifiers with caveats
  (range request limitation, roots vs entities distinction)
- changelog v0.41: add entries for strategy modifiers, pin add/update
  fast-provide, and hardened strategy parsing

* feat: gate providingDagService behind --fast-provide-dag

per-block providing during ipfs add is now opt-in via
--fast-provide-dag (or Import.FastProvideDAG config, default: false).

without it, only the root CID is fast-provided after add, and the
reprovide cycle handles the rest. this changes the default for
Provide.Strategy=pinned: previously every block was provided during
write, now only the root is immediate.

use --fast-provide-dag=true to restore the previous behavior.
Provide.Strategy=all is unaffected (blockstore hook provides on Put).

* feat(pin): expose --fast-provide-root and --fast-provide-wait flags

pin add and pin update now accept the same --fast-provide-root and
--fast-provide-wait CLI flags as ipfs add and ipfs dag import,
with the same config fallbacks (Import.FastProvideRoot,
Import.FastProvideWait).

previously these were config-only with no CLI override.

* feat: wire --fast-provide-dag across all content commands

--fast-provide-dag now available on ipfs add, ipfs dag import,
ipfs pin add, and ipfs pin update (matching --fast-provide-root).

- ExecuteFastProvideDAG accepts []cid.Cid so multiple roots share
  one bloom tracker (cross-root dedup for dag import and pin add)
- --fast-provide-dag supersedes --fast-provide-root (DAG walk
  includes the root CID as the first emitted via DFS pre-order)
- wait parameter: when true blocks until walk completes, when false
  runs in background goroutine
- Import.FastProvideDAG config option (default: false)

* docs(config): improve Provide.Strategy docs, add Import.FastProvideDAG

- strategy section: clearer trade-offs, suggested configurations,
  memory comparison with concrete numbers
- Import.FastProvideDAG: new config option documentation
- Import.FastProvideRoot/Wait: updated to mention pin commands
- all three Import.FastProvide* options: consistent "Applies to" lists

* chore: gofumpt and gci formatting

* chore: update boxo to latest feat/provide-entity-roots-with-dedup

* feat: TEST_DHT_STUB with ephemeral DHT peers

when TEST_DHT_STUB=1, the CLI test harness creates 20 in-process
libp2p hosts on loopback, each running a DHT server with a shared
in-memory ProviderStore. kubo daemons bootstrap to them over real
TCP, exercising the full DHT code path without public internet.

tests opt in via h.SetStubBootstrap(nodes) after Init().

on the daemon side, WAN DHT filters (AddressFilter, QueryFilter,
RoutingTableFilter, RoutingTablePeerDiversityFilter) are lifted
to accept loopback peers when TEST_DHT_STUB is set.

depends on: github.com/libp2p/go-libp2p-kad-dht#1241

* test: harden provider strategy tests

add sweep reprovide tests for all strategies (all, pinned, roots,
mfs, pinned+mfs). each test waits for two reprovide cycles to
confirm the schedule runs repeatedly. sweep uses short
Provide.DHT.Interval and polls provide stat --enc=json.

harden negative assertions:
- roots: test excludes child blocks of a recursive pin (not just
  unpinned content), using --only-hash to learn the child CID
- mfs: test that pinned content outside MFS is not provided

fix: ipfs add --only-hash no longer triggers fast-provide or
pinning (was providing CIDs for data that was never stored)

rename SetStubBootstrap to BootstrapWithStubDHT with lazy-init
(ephemeral peers created on first call, not on harness creation)

* test: add +unique and +entities strategy tests

strategy tests for pinned+mfs+unique and pinned+mfs+entities,
covering both provide-at-add-time and reprovide (two cycles).
content uses a nested DAG (root/subdir/largefile with 1 MiB
chunks) to exercise the walker on multi-level structures.

BootstrapWithStubDHT is now self-contained: it always creates
20 ephemeral DHT peers on loopback and sets TEST_DHT_STUB=1 on
each node's environment so the daemon lifts WAN DHT filters.
no external env var needed. the sweep provider requires >=20
DHT peers to estimate network size (prefix length); without
enough peers it stays offline and never provides.

TEST_DHT_STUB on the daemon side lifts WAN DHT filters
(AddressFilter, QueryFilter, RoutingTableFilter,
RoutingTablePeerDiversityFilter) to accept loopback peers.
this is set automatically by BootstrapWithStubDHT.

other changes:
- Provide.DHT.Interval=30s in sweep reprovide tests (was 1m)
- uniq() helper for unique CIDs across parallel subtests
- ipfs add --only-hash disables fast-provide and pinning

* docs: improve help text and changelog accuracy

ipfs add --help: rewrite fast-provide section with clear structure
(content discoverability, flag defaults, strategy=all behavior)

ipfs routing reprovide: mark as deprecated, note it returns an error
with sweep provider, log error with actionable guidance

changelog: fix missing --fast-provide-dag flag on pin commands,
use "routing system" instead of "DHT" where applicable, link to
docs/config.md as source of truth for defaults

environment-variables.md: note that BootstrapWithStubDHT sets
TEST_DHT_STUB automatically, no external env var needed

* chore: revert go-libp2p-kad-dht to released v0.39.0

the fork (NoopMessageSender, MsgSenderBuilder) is no longer used.
the ephemeral peer pool in BootstrapWithStubDHT replaced the
NoopMessageSender approach.

* feat: log bloom dedup stats after provide cycles

log providedCIDs and skippedBranches after each unique reprovide
cycle and fast-provide-dag walk.

tests verify exact counts with two dir pins sharing a 10 KiB file
(5 KiB chunks): fast-provide-dag asserts 5 provided + 1 skipped
branch, reprovide asserts 6 provided + 1 skipped branch (includes
empty MFS root pin). both assert bloom tracker created and no
autoscale.

updates boxo to pick up Deduplicated() counter, bloom
creation/autoscale logging, and review feedback fixes.

* chore(deps): switch boxo to post-merge commit

boxo#1124 landed on master; point to the merge commit
instead of the PR branch.

* fix(coreapi): drop providingDagService wrap

ipfs add --pin --fast-provide-dag wrapped the DAGService with
providingDagService, which announced every block as it was written
regardless of strategy modifiers. ExecuteFastProvideDAG ran in
parallel as the post-add walker. Net effect:

- pinned+entities: chunks reached the DHT despite +entities saying
  they should be skipped (correctness bug)
- pinned+unique: every block announced twice; the post-walk bloom
  only dedups against its own pass
- pinned (plain): every block announced twice

ExecuteFastProvideDAG already has bloom dedup, entity-roots support,
and unbuffered backpressure, so it is now the single mechanism for
--fast-provide-dag across ipfs add, dag import, pin add, and pin
update.

Provide.Strategy=all is untouched: every block is provided at the
blockstore level via the blockstore.Provider hook in
core/node/storage.go, which is independent of coreapi. The Pinned
strategy bit gated providingDagService and the parser rejects
combining "all" with other strategies, so "all" never set that bit
in the first place.

- core/coreapi/unixfs.go: drop the wrap, the providingDagService
  struct, and the now-unused mh and boxo/provider imports
- core/coreiface/options/unixfs.go: drop FastProvideDAG option
- core/coreapi/coreapi.go: drop now-dead providingStrategy field
- core/commands/add.go: drop the FastProvideDAG option pass-through
- test/cli/provider_test.go: regression test using ipfs add
  --fast-provide-dag with pinned+entities -- fails on the previous
  code and passes here

* feat(config): add Provide.BloomFPRate

Operators tuning +unique or +entities strategies on memory-constrained
or extra-large repos previously had no way to trade bloom filter memory
against false-positive rate -- both the reprovide cycle and
fast-provide-dag walks hardcoded walker.DefaultBloomFPRate.

Provide.BloomFPRate is the target false positive rate (1/N) for the
shared bloom tracker. Has no effect on Provide.Strategy=all or other
strategies that do not walk DAGs through the tracker. Validation
rejects values below 1_000_000 (~1 in 1M); below that the bloom
becomes lossy enough to drop a meaningful fraction of CIDs from each
reprovide cycle.

The single source of truth for the default value is
config.DefaultProvideBloomFPRate; docs reference it descriptively
(~1 in 4.75M, ~4 bytes/CID) so the literal lives in exactly one place.

- config/provide.go: BloomFPRate field, DefaultProvideBloomFPRate
  and MinProvideBloomFPRate constants, validation
- config/provide_test.go: round-trip + validation cases
- core/node/provider.go: plumb fpRate through setReproviderKeyProvider
  and createKeyProvider
- core/commands/cmdenv/env.go: ExecuteFastProvideDAG takes fpRate
- core/commands/{add,dag/import,pin/pin}.go: resolve from cfg and
  pass through to ExecuteFastProvideDAG
- docs/config.md: new Provide.BloomFPRate section after Provide.DHT.*
  with memory tradeoff table and minimum-value note
- docs/changelogs/v0.41.md: link to the new option from the +unique/
  +entities section

* test(node): cover unique count persistence

readLastUniqueCount and persistUniqueCount were exercised only
indirectly via CLI tests, leaving the 8-byte length check and the
"missing key" fallback without direct coverage.

- empty datastore returns 0 (no previous cycle)
- round trip across the full uint64 range (0, 1, 1k, 1M, 1B, MaxUint64)
- overwrite returns the most recent value (matches per-cycle persist)
- corrupt length (empty, short, long, single byte) returns 0 instead
  of panicking

* fix(cmdenv): tie async fast-provide to node ctx

Background fast-provide goroutines were implicitly bound to
req.Context, which go-ipfs-cmds cancels on handler exit, so
async --fast-provide-dag (and --fast-provide-root parented on
context.Background) aborted or outlived the node. Parent both
paths off the IpfsNode lifetime context instead.

- ExecuteFastProvideRoot: async goroutine now derives from
  ipfsNode.Context(), so it cancels on daemon shutdown rather
  than potentially touching a closed DHT client.
- ExecuteFastProvideDAG: takes cmdCtx and nodeCtx; wait=true
  runs inline under cmdCtx (Ctrl+C still cancels the walk),
  wait=false runs in a goroutine under nodeCtx so the walk
  survives command exit but still stops on shutdown.
- add, dag import, pin add/update: pass node.Context() as
  the new nodeCtx argument.
- changelog: note the behavior change for opt-in strategies.

* test(cli): cover async fast-provide-dag walk

Adds TestProviderFastProvideDAGAsyncSurvives: ipfs add with
--fast-provide-dag=true but no --fast-provide-wait must walk the
full DAG in a background goroutine that outlives the command
handler, announce every block, and leave chunk CIDs findable by
peers via findprovs. A long Provide.DHT.Interval ensures the
scheduled reprovide cycle cannot be the source of the chunk
announcements.

* docs(changelog): tighten v0.41 provide section
* fix: check provide strategy before providing newly added filestore blocks

currently, all blocks added into filestore is provided regardless of which provide strategy is selected, which makes little sense when the current strategy is not "all" (new files might not be pinned with --pin=false or might not be added to MFS). Added a similar check as the blockstore to make the behavior identical between blockstore and filestore.

* docs: improve filestore provide strategy changelog entry

- reword heading and description for clarity
- link to filestore and urlstore experiment docs
- mention both experiments since both use the same code path

* test: verify filestore respects Provide.Strategy

- add positive/negative test pair for filestore provide gating
- positive: filestore + "all" strategy provides root and leaf CIDs
- negative: filestore + "roots" strategy with --pin=false does not
- increase providerTimeout to 30s for CI reliability
- replace fixed 500ms DHT sleep with waitForProviderReady: polls
  'ipfs provide stat' for SweepingProvider connectivity, then runs
  a canary provide+findprovs round-trip
- add expectNoneProvided for parallel negative assertions to avoid
  sequential timeout accumulation

* docs(changelog): trim filestore section

---------

Co-authored-by: Marcin Rataj <lidel@lidel.org>
* feat: add built-in `ipfs update` command

adds `ipfs update` command tree that downloads pre-built Kubo binaries
from GitHub Releases, verifies SHA-512 checksums, and replaces the
running binary in place.

subcommands:

- `ipfs update check` -- query GitHub for newer versions
- `ipfs update versions` -- list available releases
- `ipfs update install [version]` -- download, verify, backup, and
  atomically replace the current binary
- `ipfs update revert` -- restore the previously backed up binary
  from `$IPFS_PATH/old-bin/`

read-only subcommands (check, versions) work while the daemon is
running. install and revert require the daemon to be stopped first.

design decisions:

- uses GitHub Releases API instead of dist.ipfs.tech because GitHub
  is harder to censor in regions that block IPFS infrastructure
- honors GITHUB_TOKEN/GH_TOKEN to avoid unauthenticated rate limits
- backs up the current binary before replacing, with permission-error
  fallback that saves to a temp dir with manual `sudo mv` instructions
- `KUBO_UPDATE_GITHUB_URL` env var redirects API calls for integration
  testing; `IPFS_VERSION_FAKE` overrides the reported version
- unit tests use mock HTTP servers and the var override; CLI tests use
  the env vars with a temp binary copy so the real build is never
  touched

resolves #10937

* fix(update): harden download and extraction

- cap decompressed binary at 1 GB to block zip/tar bombs
- propagate tar.gz/zip errors instead of swallowing them
- fall back to 1h context timeout when --timeout is not set
- warn on stderr when daemon lock check fails
- clarify that fetch+verify+extract complete before touching binary

* fix(update): resolve binary path on windows

The test harness hardcodes the binary path as `cmd/ipfs/ipfs`
without the `.exe` suffix. On Windows the built binary is
`ipfs.exe`, so copyBuiltBinary needs to append the extension.

* fix(update): handle windows binary locking in install test

On Windows the OS locks the running executable, so atomicfile cannot
rename over it. The install command falls back to saving the new binary
to a temp path. Accept both outcomes in TestUpdateInstall: in-place
replacement (Unix) or permission-denied fallback (Windows). Also fix
stash path to include .exe suffix on Windows.

- test/cli/update_test.go: branch on runtime.GOOS for install assertions
- test/sharness/t0063-external.sh: remove, tested the old ExternalBinary
  delegation which is replaced by the built-in update command
- .github/workflows/test-migrations.yml: pass GITHUB_TOKEN to avoid rate limits

* fix(test): handle windows EINVAL on process signal after wait

On Windows, Process.Wait() sets the handle state to "released" rather
than "done", so a subsequent Signal() returns syscall.EINVAL instead
of os.ErrProcessDone. This caused StopDaemon cleanup to panic on
Windows CI. Treat both errors as "process already exited".

* feat(update): add 'clean' subcommand

Drops every backed-up Kubo binary from $IPFS_PATH/old-bin/ so users can
reclaim disk space without hand-deleting files. Safe with the daemon
running, only touches the backup directory.

- update.go: extract stashDirName const, factor out listStashes()
  helper, add updateCleanCmd
- commands_test.go: register /update/clean
- test/cli/update_test.go: TestUpdateClean covers removal, empty dir,
  json output, and preservation of unrelated files

* docs(changelog): tighten ipfs update entry

Drop the marketing opener, the duplicate install example, and the
revert/versions sentence; all are covered by 'ipfs update --help'.
Mention the new 'clean' subcommand in the trailing pointer.

* fix(test): skip fuse cli tests on non-unix

Both fuse_test.go and realworld_test.go rely on Unix-only APIs
(syscall.Truncate, POSIX tools). The sibling xattr_*_test.go files
were already gated, but these two compiled everywhere, so any workflow
running 'go test ./test/cli/...' on Windows hit 'undefined: syscall.Truncate'.

Use the same '(linux || darwin || freebsd) && !nofuse' constraint that
the fuse/ packages already use so platform gating is consistent.

* fix(test): run install/revert sequentially

TestUpdateInstall and TestUpdateRevert write a copy of the ipfs
binary and then exec it. When other tests run in parallel, a
concurrent fork() can inherit the still-open write fd into its
child, leaving the freshly written file 'text file busy' for exec
until the sibling child execs.

Dropping t.Parallel() on these two tests ensures no other goroutine
is mid-fork while the binary is being written, which is the only
reliable way to avoid the ETXTBSY race without clever fd tricks.

* ci(update): use cloudflare/google DNS on macos

GitHub's macOS runners intermittently lose DNS for api.github.com,
which fails the real-network subtests in TestUpdate. Point the
resolver at 1.1.1.1 and 8.8.8.8 on every active network service
and flush the DNS cache before running the update tests.

* fix(update): fsync before close in atomicfile and stash

* fix(update): use unique temp file in permission fallback

The previous fallback wrote to a predictable path (/tmp/ipfs-<ver>),
which on shared systems lets a local attacker pre-create the path
as a symlink and steer the user's subsequent 'sudo mv' anywhere.
Switch to os.CreateTemp so the path is unique and exclusively owned
by this process.

* refactor(update): rename test env vars to TEST_KUBO_*

IPFS_VERSION_FAKE and KUBO_UPDATE_GITHUB_URL are test-only escape
hatches with no production use case. The TEST_ prefix signals this
clearly and reduces the chance of accidental use in production.

- IPFS_VERSION_FAKE      -> TEST_KUBO_VERSION
- KUBO_UPDATE_GITHUB_URL -> TEST_KUBO_UPDATE_GITHUB_URL

* style(update): unshadow err in stashBinary

* fix(update): warn when IPFS path can't be resolved

silently skipping the daemon lock check on path-resolution failure can
mask a misconfigured IPFS_PATH; print a warning so the user notices
before the install proceeds.

* fix(update): revert atomicfile Sync, use errors.Is for EOF

Revert the Sync() addition in atomicfile.Close() to avoid widening
the failure surface for existing migration callers that panic on
Close errors (Must(out.Close()) in WithBackup). The stashBinary
fsync in update.go is kept since that code path is new.

- revert repo/fsrepo/migrations/atomicfile/atomicfile.go to master
- use errors.Is(err, io.EOF) in extractFromTarGz
* fix(fuse/mfs): implement Statfs to fix "not enough space" error on macOS

Co-authored-by: Marcin Rataj <lidel@lidel.org>
* fix: --cid-base works in all commands and auto-upgrades CIDv0

Passing --cid-base=base32 now returns CIDv1 in base32 everywhere,
including block, dag stat, and object patch which previously ignored it.

- cidbase: auto-upgrade CIDv0 when base is not base58btc, deprecate
  --upgrade-cidv0-in-output, remove GetLowLevelCidEncoder
- block stat/put/rm: use GetCidEncoder
- dag stat: store CID as pre-encoded string, drop MarshalJSON/UnmarshalJSON
- object patch rm-link/add-link: use GetCidEncoder
- bitswap: switch to GetCidEncoder

* test: add harness tests for --cid-base flag

Remove unused DagStat.String() which truncated CIDs.
Add CLI tests for --cid-base across block, dag stat,
and object patch commands, including the --format=v0
interaction.

* fix: respect --cid-base in refs local, object diff, pin remote, files chroot

Use GetCidEncoder in commands that were still outputting CIDs
via raw .String() calls.

- refs local: encode blockstore keys with the requested base
- object diff: encode Before/After CIDs in text encoder
- pin remote add/ls: pass encoder through toRemotePinOutput
- files chroot: encode old/new root CIDs in status message
- tests: use base16 to avoid false positives if base32 becomes default

* docs: update changelog entry for --cid-base fixes

* test: cover --cid-base for add, pin ls, dag import

Add harness tests for add, add -Q, pin ls, and dag import.
Fix object patch tests broken by upstream UnixFS validation.
Use base16 in all tests to avoid false positives.

* docs: add metrics and CARv2 highlights to v0.41 changelog
Two sequential syscall.Statfs calls can return different block counts
when CI writes change the filesystem between calls.

- add fusetest.AssertStatfsNonZero helper for integration tests
- assert non-zero Blocks and Bfree <= Blocks instead of exact match
- use the helper in ipns, mfs, readonly, writable Statfs tests
@lidel lidel added the skip/changelog This change does NOT require a changelog entry label Apr 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip/changelog This change does NOT require a changelog entry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants